{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ThinkDSP\n",
    "\n",
    "This notebook contains solutions to exercises in Chapter 7: Discrete Fourier Transform\n",
    "\n",
    "Copyright 2015 Allen Downey\n",
    "\n",
    "License: [Creative Commons Attribution 4.0 International](http://creativecommons.org/licenses/by/4.0/)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "PI2 = 2 * np.pi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 1\n",
    "\n",
    "In this chapter, I showed how we can express the DFT and inverse DFT\n",
    "as matrix multiplications.  These operations take time proportional to\n",
    "$N^2$, where $N$ is the length of the wave array.  That is fast enough\n",
    "for many applications, but there is a faster\n",
    "algorithm, the Fast Fourier Transform (FFT), which takes time\n",
    "proportional to $N \\log N$.\n",
    "\n",
    "The key to the FFT is the Danielson-Lanczos lemma:\n",
    "\n",
    "$DFT(y)[n] = DFT(e)[n] + \\exp(-2 \\pi i n / N) DFT(o)[n]$\n",
    "\n",
    "Where $ DFT(y)[n]$ is the $n$th element of the DFT of $y$; $e$ is the even elements of $y$, and $o$ is the odd elements of $y$.\n",
    "\n",
    "This lemma suggests a recursive algorithm for the DFT:\n",
    "\n",
    "1. Given a wave array, $y$, split it into its even elements, $e$, and its odd elements, $o$.\n",
    "\n",
    "2. Compute the DFT of $e$ and $o$ by making recursive calls.\n",
    "\n",
    "3. Compute $DFT(y)$ for each value of $n$ using the Danielson-Lanczos lemma.\n",
    "\n",
    "For the base case of this recursion, you could wait until the length\n",
    "of $y$ is 1.  In that case, $DFT(y) = y$.  Or if the length of $y$\n",
    "is sufficiently small, you could compute its DFT by matrix multiplication,\n",
    "possibly using a precomputed matrix.\n",
    "\n",
    "Hint: I suggest you implement this algorithm incrementally by starting\n",
    "with a version that is not truly recursive.  In Step 2, instead of\n",
    "making a recursive call, use `dft` or `np.fft.fft`.  Get Step 3 working,\n",
    "and confirm that the results are consistent with the other\n",
    "implementations.  Then add a base case and confirm that it works.\n",
    "Finally, replace Step 2 with recursive calls.\n",
    "\n",
    "One more hint: Remember that the DFT is periodic; you might find `np.tile` useful.\n",
    "\n",
    "You can read more about the FFT at https://en.wikipedia.org/wiki/Fast_Fourier_transform."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As the test case, I'll start with a small real signal and compute its FFT:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ 0.2+0.j  -1.2-0.2j  0.2+0.j  -1.2+0.2j]\n"
     ]
    }
   ],
   "source": [
    "ys = [-0.5, 0.1, 0.7, -0.1]\n",
    "hs = np.fft.fft(ys)\n",
    "print(hs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's my implementation of DFT from the book:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [],
   "source": [
    "def dft(ys):\n",
    "    N = len(ys)\n",
    "    ts = np.arange(N) / N\n",
    "    freqs = np.arange(N)\n",
    "    args = np.outer(ts, freqs)\n",
    "    M = np.exp(1j * PI2 * args)\n",
    "    amps = M.conj().transpose().dot(ys)\n",
    "    return amps"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can confirm that this implementation gets the same result."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "5.864775846765962e-16"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hs2 = dft(ys)\n",
    "np.sum(np.abs(hs - hs2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As a step toward making a recursive FFT, I'll start with a version that splits the input array and uses np.fft.fft to compute the FFT of the halves."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [],
   "source": [
    "def fft_norec(ys):\n",
    "    N = len(ys)\n",
    "    He = np.fft.fft(ys[::2])\n",
    "    Ho = np.fft.fft(ys[1::2])\n",
    "    \n",
    "    ns = np.arange(N)\n",
    "    W = np.exp(-1j * PI2 * ns / N)\n",
    "    \n",
    "    return np.tile(He, 2) + W * np.tile(Ho, 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we get the same results:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.0"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hs3 = fft_norec(ys)\n",
    "np.sum(np.abs(hs - hs3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, we can replace `np.fft.fft` with recursive calls, and add a base case:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [],
   "source": [
    "def fft(ys):\n",
    "    N = len(ys)\n",
    "    if N == 1:\n",
    "        return ys\n",
    "    \n",
    "    He = fft(ys[::2])\n",
    "    Ho = fft(ys[1::2])\n",
    "    \n",
    "    ns = np.arange(N)\n",
    "    W = np.exp(-1j * PI2 * ns / N)\n",
    "    \n",
    "    return np.tile(He, 2) + W * np.tile(Ho, 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And we get the same results:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": false,
    "jupyter": {
     "outputs_hidden": false
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1.6653345369377348e-16"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hs4 = fft(ys)\n",
    "np.sum(np.abs(hs - hs4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This implementation of FFT takes time proportional to $n \\log n$.  It also takes space proportional to $n \\log n$, and it wastes some time making and copying arrays.  It can be improved to run \"in place\"; in that case, it requires no additional space, and spends less time on overhead."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true,
    "jupyter": {
     "outputs_hidden": true
    }
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
